﻿using System;
using System.Net;
using System.Net.Sockets;

namespace Yuki.Sockets
{
    public abstract class ServerSocket
    {
        private int backlog;
        private int clientbuffersize = 0xffff;
        private Socket Connection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        private bool enabled;
        public SocketEvent<YukiSocketClient, object> OnClientConnect;
        public SocketEvent<YukiSocketClient, object> OnClientDisconnect;
        public SocketEvent<YukiSocketClient, SocketError> OnClientError;
        public SocketEvent<YukiSocketClient, byte[]> OnClientReceive;
        private ushort port;

        private void AsyncConnect(IAsyncResult res)
        {
            byte ConnectionPhase = 0;
            YukiSocketClient Client = null;
            try
            {
                Client = new YukiSocketClient(this, this.Connection.EndAccept(res), this.clientbuffersize) {
                    Crypto = this.MakeCrypto(),
                    Connected = true
                };
                ConnectionPhase = (byte) (ConnectionPhase + 1);
                if (this.OnClientConnect != null)
                {
                    this.OnClientConnect(Client, null);
                }
                this.Connection.BeginAccept(new AsyncCallback(this.AsyncConnect), null);
                Client.Connection.BeginReceive(Client.Buffer, 0, Client.Buffer.Length, SocketFlags.None, new AsyncCallback(this.AsyncReceive), Client);
                ConnectionPhase = (byte) (ConnectionPhase + 1);
            }
            catch (SocketException)
            {
                if (this.enabled)
                {
                    this.Connection.BeginAccept(new AsyncCallback(this.AsyncConnect), null);
                }
            }
            catch (ObjectDisposedException)
            {
            }
        }

        private void AsyncReceive(IAsyncResult res)
        {
            try
            {
                SocketError Error;
                YukiSocketClient Client = (YukiSocketClient) res.AsyncState;
                Client.RecvSize = Client.Connection.EndReceive(res, out Error);
                if ((Error == SocketError.Success) && (Client.RecvSize > 0))
                {
                    byte[] dest = new byte[Client.RecvSize];
                    if (Client.Crypto != null)
                    {
                        Client.Crypto.Decrypt(Client.Buffer, dest, dest.Length);
                    }
                    else
                    {
                        unsafe
                        {
                            Native.memcpy(dest, Client.Buffer, Client.RecvSize);
                        }
                    }
                    if (this.OnClientReceive != null)
                    {
                        this.OnClientReceive(Client, dest);
                    }
                    Client.Connection.BeginReceive(Client.Buffer, 0, Client.Buffer.Length, SocketFlags.None, new AsyncCallback(this.AsyncReceive), Client);
                }
                else
                {
                    Client.Connected = false;
                    if ((Error != SocketError.Success) && (this.OnClientError != null))
                    {
                        this.OnClientError(Client, Error);
                    }
                    this.InvokeDisconnect(Client);
                }
            }
            catch (SocketException)
            {
            }
            catch (ObjectDisposedException)
            {
            }
        }

        public void Disable()
        {
            if (this.enabled)
            {
                this.Connection.Close();
                this.enabled = false;
            }
        }

        public void Enable()
        {
            if (!this.enabled)
            {
                this.Connection.Bind(new IPEndPoint(IPAddress.Any, this.port));
                this.Connection.Listen(this.backlog);
                this.Connection.BeginAccept(new AsyncCallback(this.AsyncConnect), null);
                this.enabled = true;
            }
        }

        private void enabledCheck(string Variable)
        {
            if (this.enabled)
            {
                throw new Exception("Cannot modify " + Variable + " while socket is enabled.");
            }
        }

        public void InvokeDisconnect(YukiSocketClient Client)
        {
            if (Client != null)
            {
                try
                {
                    if (Client.Connected)
                    {
                        Client.Connection.Shutdown(SocketShutdown.Both);
                        Client.Connection.Close();
                    }
                    else
                    {
                        if (this.OnClientDisconnect != null)
                        {
                            this.OnClientDisconnect(Client, null);
                        }
                        Client.Wrapper = null;
                        Client = null;
                    }
                }
                catch (ObjectDisposedException)
                {
                }
            }
        }

        protected abstract IPacketCipher MakeCrypto();

        public int Backlog
        {
            get
            {
                return this.backlog;
            }
            set
            {
                this.enabledCheck("Backlog");
                this.backlog = value;
            }
        }

        public int ClientBufferSize
        {
            get
            {
                return this.clientbuffersize;
            }
            set
            {
                this.enabledCheck("ClientBufferSize");
                this.clientbuffersize = value;
            }
        }

        public bool Enabled
        {
            get
            {
                return this.enabled;
            }
        }

        public ushort Port
        {
            get
            {
                return this.port;
            }
            set
            {
                this.enabledCheck("Port");
                this.port = value;
            }
        }
    }
}

